﻿// Copyright (C) Jari Hautio <jari.hautio@iki.fi> 2008. Licensed under GPLv2. See LICENSE.txt in solution folder.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Text;
using System.Windows.Forms;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;

namespace RyijyApp
{
    /// <summary>
    /// Drawing Canvas implements basic pixel drawing operations in 
    /// scrollable and zoomable window.
    /// </summary>
    public partial class DrawingCanvas : UserControl
    {
        public interface DrawingLayer
        {
            void Render(Graphics g, RectangleF src, RectangleF to, float zoom);
            event EventHandler RequestRedraw;
        }

        /// <summary>
        /// Drawing target picture shown in the control
        /// </summary>
        private Bitmap picture;

        /// <summary>
        /// disables image update callback processing.
        /// </summary>
        bool disableUpdate = false;
        /// <summary>
        /// Set or get target picture for drawing
        /// </summary>
        public Bitmap Picture 
        {
            get 
            { 
                return picture; 
            } 
            set 
            { 
                picture = value;
                InitImage();
            } 
        }

        /// <summary>
        /// Calculate best zoom factor for fitting the image to given width and height
        /// </summary>
        /// <param name="w">Canvas width</param>
        /// <param name="h">Canvas height</param>
        /// <returns></returns>
        float CalcFit(int w, int h)
        {
            return picture == null ? 1.0F :
              Math.Min(1.0F * w / picture.Width, 1.0F * h / picture.Height);
        }
        /// <summary>
        /// Zooming factor.
        /// </summary>
        float zoom = 1;
        public float Zoom
        {
            get { return zoom; }
            set
            {
                // fit to window
                if (value <= 0)
                {
                    if (picture!=null)
                    {
                        zoom = CalcFit(panel1.Width, panel1.Height);
                    }
                    else zoom = 1;
                    fitToWindow = true;
                }
                else
                {
                    zoom = value;
                    fitToWindow = false;
                }                
                SetScrolls();
                UpdateImage(true);
            }
        }
        public Cursor PictureCursor { get { return pictureBox1.Cursor; } set{pictureBox1.Cursor = value;} }
        public const float ZoomFitWindow = 0;
        bool fitToWindow = false;
        /// <summary>
        /// get or set auto fit mode, where resizing keeps image fit to window
        /// </summary>
        public bool FitToWindow { get { return fitToWindow; } set { fitToWindow = value; Zoom = ZoomFitWindow; } }

        public enum GridStyles { None, Dash, Black, White };
        GridStyles gridStyle = GridStyles.None;
        public GridStyles GridStyle { get { return gridStyle; } set { gridStyle = value; gridLayer.Style = value; } }
        public int GridLimit { get { return gridLayer.GridLimit; } set { gridLayer.GridLimit = value; } }
        GridLayer gridLayer = new GridLayer();
        /// <summary>
        /// Applies zoom factor to value and returns ceiling integer value.
        /// </summary>
        int Zoomed(int value) { return Convert.ToInt32(Math.Ceiling(value * zoom)); }

        CropCueLayer cropLayer = new CropCueLayer();
        public CropCueLayer CropCue { get { return cropLayer; } }

        /// <summary>
        /// Image layer with main picture.
        /// </summary>
        ImageLayer imageLayer = new ImageLayer();

        SymbolLayer symbolLayer = new SymbolLayer();

        /// <summary>
        /// List of active drawing layers. 
        /// Layers are drawn starting from first layer in the list.
        /// </summary>
        List<DrawingLayer> layers = new List<DrawingLayer>();

        public enum SymbolMode { None, Symbols, ColorSymbol, SmallSymbols };
        public SymbolMode SymbolDrawingMode
        {
            get { return symbolLayer.Mode; }
            set 
            {
                disableUpdate = true;
                symbolLayer.Mode = value;
                switch (value)
                {
                    case SymbolMode.None:
                        imageLayer.Matrix = null;
                        break;
                    case SymbolMode.SmallSymbols:
                        imageLayer.Matrix = null;
                        break;
                    case SymbolMode.ColorSymbol:
                        imageLayer.Matrix = new ColorMatrix(
                          new float[][]
                          {
                             new float[] {0.5f, 0f  , 0f  , 0, 0},
                             new float[] {0f  , 0.5f, 0f  , 0, 0},
                             new float[] {0f  , 0f  , 0.5f, 0, 0},
                             new float[] {0   , 0   , 0   , 1, 0},
                             new float[] {0.5F, 0.5F, 0.5F, 0, 1}
                          });
                        break;
                    case SymbolMode.Symbols:
                        imageLayer.Matrix = new ColorMatrix(
                          new float[][]
                          {
                             new float[] {0, 0, 0, 0, 0},
                             new float[] {0, 0, 0, 0, 0},
                             new float[] {0, 0, 0, 0, 0},
                             new float[] {0, 0, 0, 1, 0},
                             new float[] {1, 1, 1, 0, 1}
                          });
                        break;
                }
                disableUpdate = false;
                UpdateImage(false);
            }
        }
        public SymbolMap Symbols { get { return symbolLayer.Symbols; } }
        public ColorMatrix ImageColorMatrix { get { return imageLayer.Matrix;} set {imageLayer.Matrix = value;}}

        /// <summary>
        /// Get or set color that will have empty symbol.
        /// </summary>
        public Color EmptySymbolColor
        {
            get { return symbolLayer.EmptySymbolColor; }
            set { symbolLayer.EmptySymbolColor= value; }
        }
	
        /// <summary>
        /// Constructs new Drawing canvas
        /// </summary>
        public DrawingCanvas()
        {
            InitializeComponent();
            layers.Add(imageLayer);
            layers.Add(symbolLayer);
            layers.Add(gridLayer);
            layers.Add(cropLayer);
            foreach(DrawingLayer l in layers)
                l.RequestRedraw += new EventHandler(layer_RequestRedraw);
        }

        void layer_RequestRedraw(object sender, EventArgs e)
        {
            UpdateImage(false);
        }

        /// <summary>
        /// Get pixel color value at given mouse coordinates.
        /// </summary>
        /// <param name="p">Target point relative to upper left corner (0,0).</param>
        /// <returns>Returns Color value at point or Color.Empty.</returns>
        public Color GetPixel(Point p)
        {
            int x = (Convert.ToInt32(Math.Floor(p.X/zoom)) + hScrollBar1.Value) ;
            int y = (Convert.ToInt32(Math.Floor(p.Y / zoom)) + vScrollBar1.Value);
            Color c = Color.Empty;
            if (picture != null &&
                x >= 0 && y >= 0 && 
                x < picture.Width && y < picture.Height)
                c = picture.GetPixel(x, y);
            return c;

        }

        /// <summary>
        /// Converts mouse coordinates relative to upper left corner to 
        /// image coordinates
        /// </summary>
        /// <param name="p"></param>
        /// <returns></returns>
        public Point ConvertToImage(Point p)
        {
            int x = (Convert.ToInt32(Math.Floor(p.X / zoom)) + hScrollBar1.Value);
            int y = (Convert.ToInt32(Math.Floor(p.Y / zoom)) + vScrollBar1.Value);
            return new Point(x, y);
        }
        /// <summary>
        /// Set pixel color value at given mouse coordinates.
        /// </summary>
        /// <param name="p">Target point relative to upper left corner (0,0).</param>
        /// <param name="color">Target color.</param>
        public void SetPixel(Point p, Color color)
        {
            Point pt = ConvertToImage(p);
            Color c = Color.Empty;
            if (picture != null &&
                pt.X >= 0 && pt.Y >= 0 &&
                pt.X < picture.Width && pt.Y < picture.Height)
                picture.SetPixel(pt.X, pt.Y, color);
            UpdateImage(false);
        }

        /// <summary>
        /// Initialize control for new image.
        /// </summary>
        private void InitImage()
        {
            bool disabled = false;
            if (!disableUpdate)
                disableUpdate = true;
            imageLayer.Picture = picture;
            symbolLayer.Picture = picture;
            Zoom = fitToWindow ? ZoomFitWindow : 1;
            if (disabled)
            {
                disableUpdate = false;
                UpdateImage(false);
            }
        }
        /// <summary>
        /// Set scrollbar sizes and adjust values to proper range.
        /// </summary>
        private void SetScrolls()
        {
            hScrollBar1.Minimum = 0;
            vScrollBar1.Minimum = 0;
            hScrollBar1.Maximum = picture != null ? picture.Width : 1;
            vScrollBar1.Maximum = picture != null ? picture.Height : 1;
            hScrollBar1.LargeChange = Convert.ToInt32(Math.Ceiling( panel1.Width / zoom ));
            vScrollBar1.LargeChange = Convert.ToInt32(Math.Ceiling( panel1.Height / zoom ));
            if (picture != null)
            {
                disableUpdate = true;
                if (hScrollBar1.Value + hScrollBar1.LargeChange > picture.Width)
                    hScrollBar1.Value = Math.Max(0, picture.Width - hScrollBar1.LargeChange);
                if (vScrollBar1.Value + vScrollBar1.LargeChange > picture.Height)
                    vScrollBar1.Value = Math.Max(0, picture.Height - vScrollBar1.LargeChange);
                disableUpdate = false;
            }
        }
        /// <summary>
        /// Render full image to graphics
        /// </summary>
        /// <param name="g"></param>
        /// <param name="w"></param>
        /// <param name="h"></param>
        public void Render(Graphics g, int w, int h)
        {
            Render(g, 0,0,w, h, CalcFit(w, h));
        }
        /// <summary>
        /// Render image to graphics with zoom
        /// </summary>
        /// <param name="g"></param>
        /// <param name="w">Target image width</param>
        /// <param name="h">Target image height</param>
        /// <param name="z">Zooming factor (>0)</param>
        public void Render(Graphics g, int srcx, int srcy, int w, int h, float z)
        {
            if (z == 0) return;
            float nx = (picture.Width - srcx) * z;
            float ny = (picture.Height - srcy) * z;
            // limit w and h to actual image
            if (w > nx) w = Convert.ToInt32(Math.Floor(nx));
            if (h > ny) h = Convert.ToInt32(Math.Floor(ny));

            RectangleF src = new RectangleF(srcx, srcy, w / z, h / z);
            RectangleF to = new RectangleF(0, 0, w, h);
            foreach (DrawingLayer l in layers)
            {
                l.Render(g, src, to, z);
            }
        }

        /// <summary>
        /// Updates picturebox image size large enough.
        /// </summary>
        /// <param name="w">Required width</param>
        /// <param name="h">Required height</param>
        private void UpdatePictureBoxSize(int w, int h)
        {
            // if current image is too small, then allcoate new image for picture box.
            if (pictureBox1.Image == null ||
                pictureBox1.Image.Width < w ||
                pictureBox1.Image.Height < h)
            {
                // Release old image resources before allocating new image   
                if (pictureBox1.Image != null)
                {
                    Image old = pictureBox1.Image;
                    pictureBox1.Image = null;
                    old.Dispose();
                }
                // create oversize image to keep window resizing efficient
                pictureBox1.Image = new Bitmap(w + 50, h + 50);
            }
        }
        /// <summary>
        /// Updates picture box with scaled and scrolled part
        /// of the image.
        /// </summary>
        public void UpdateImage(bool updateNow)
        {
            if (disableUpdate)
                return;
            int w = panel1.Width;
            int h = panel1.Height;

            UpdatePictureBoxSize(w, h);

            using (Graphics grPhoto = Graphics.FromImage(pictureBox1.Image))
            {
                if (picture != null)
                {
                    // Clear out of image area first.
                    float nx = (picture.Width - hScrollBar1.Value) * zoom;
                    float ny = (picture.Height - vScrollBar1.Value) * zoom;
                    using (Brush br = new SolidBrush(pictureBox1.BackColor))
                    {
                        if (h - ny > 0) grPhoto.FillRectangle(br, new RectangleF(0, ny, w, h - ny));
                        if (w - nx > 0) grPhoto.FillRectangle(br, new RectangleF(nx, 0, w - nx, ny));
                    }

                    Render(grPhoto, hScrollBar1.Value, vScrollBar1.Value, w, h, zoom);
                }
                else
                {
                    using (Brush br = new SolidBrush(pictureBox1.BackColor))
                    {
                        grPhoto.FillRectangle(br, 0, 0, w, h);
                    }
                }
            }
            pictureBox1.Invalidate();
            if (updateNow)
                pictureBox1.Update() ;

        }

        private void pictureBox1_MouseMove(object sender, MouseEventArgs e)
        {
            OnMouseMove(e);
        }

        private void pictureBox1_MouseLeave(object sender, EventArgs e)
        {

        }
        private void pictureBox1_Resize(object sender, EventArgs e)
        {
        }

        private void vScrollBar1_ValueChanged(object sender, EventArgs e)
        {
            UpdateImage(true);

        }

        private void hScrollBar1_ValueChanged(object sender, EventArgs e)
        {
            UpdateImage(true);
        }

        private void pictureBox1_MouseClick(object sender, MouseEventArgs e)
        {
            OnMouseClick(e);
        }

        private void pictureBox1_MouseDown(object sender, MouseEventArgs e)
        {
            OnMouseDown(e);
        }

        private void pictureBox1_MouseUp(object sender, MouseEventArgs e)
        {
            OnMouseUp(e);
        }

        private void DrawingCanvas_Resize(object sender, EventArgs e)
        {
            if (fitToWindow)
            {
                Zoom = ZoomFitWindow;
            }
            else
            {
                SetScrolls();
                UpdateImage(true);
            }

        }

    }

    class SymbolLayer : DrawingCanvas.DrawingLayer
    {
        Bitmap picture = null;
        public Bitmap Picture 
        {
            get { return picture; } 
            set 
            {
                picture = value;
                symbols.Reset();
                OnRequestRedraw(); 
            } 
        }
        SymbolMap symbols = new SymbolMap();
        public SymbolMap Symbols { get { return symbols; } }
        DrawingCanvas.SymbolMode mode = DrawingCanvas.SymbolMode.None;
        public DrawingCanvas.SymbolMode Mode
        {
            get { return mode; }
            set
            {
                if (mode != value)
                {
                    mode = value;
                    OnRequestRedraw();
                }
            }
        }
        private Color emptySymbolColor;
        /// <summary>
        /// Get or set color that will have empty symbol.
        /// </summary>
        public Color EmptySymbolColor
        {
            get { return emptySymbolColor; }
            set { emptySymbolColor = value; }
        }
        #region DrawingLayer Members

        public void Render(Graphics grPhoto, RectangleF src, RectangleF to, float zoom)
        {
            if (picture == null) return;
            if (mode == DrawingCanvas.SymbolMode.None) return;

            float size = (mode == DrawingCanvas.SymbolMode.SmallSymbols) ? zoom / 2.0F : zoom;

            for (int x = 0; x < Convert.ToInt32(src.Width); x++)
                for (int y = 0; y < Convert.ToInt32(src.Height);y++)
                {
                    Color c = picture.GetPixel(Convert.ToInt32(src.X)+x, Convert.ToInt32(src.Y)+y);
                    if (c == emptySymbolColor) continue;
                    symbols.DrawSymbol(grPhoto, new RectangleF(x * zoom, y * zoom, size, size), c);
                }
        }

        public event EventHandler RequestRedraw;
        #endregion
        void OnRequestRedraw() { if (RequestRedraw != null) RequestRedraw(this, EventArgs.Empty); }
    }

    class ImageLayer : DrawingCanvas.DrawingLayer
    {
        Bitmap picture = null;
        public Bitmap Picture { get { return picture; } set { picture = value; OnRequestRedraw(); } }
        private ColorMatrix colorMatrix = null;

	    public ColorMatrix Matrix
	    {
		    get { return colorMatrix;}
		    set { colorMatrix = value;}
	    }

        #region DrawingLayer Members

        public void Render(Graphics grPhoto, RectangleF src, RectangleF to, float zoom)
        {

            grPhoto.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.NearestNeighbor;
            if (picture != null)
            {
                // blit image part.
                RectangleF r = src;
                r.Offset(-0.5F, -0.5F);
                System.Drawing.Imaging.ImageAttributes attr = new System.Drawing.Imaging.ImageAttributes();
                if (colorMatrix != null)
                    attr.SetColorMatrix(colorMatrix);
                // Flipping makes borders nice
                attr.SetWrapMode(WrapMode.TileFlipXY);
                Rectangle to2 = new Rectangle(
                    Convert.ToInt32(to.X),
                    Convert.ToInt32(to.Y),
                    Convert.ToInt32(to.Width),
                    Convert.ToInt32(to.Height));
                grPhoto.DrawImage(picture, to2, r.X, r.Y, r.Width, r.Height,GraphicsUnit.Pixel, attr);
            }
        }

        public event EventHandler RequestRedraw;
        #endregion
        void OnRequestRedraw() { if (RequestRedraw != null) RequestRedraw(this, EventArgs.Empty); }
    }
    class GridLayer : DrawingCanvas.DrawingLayer
    {
        private int gridLimit = 5;
        public int GridLimit { get { return gridLimit; } set { gridLimit = value; } }
        DrawingCanvas.GridStyles gridStyle = DrawingCanvas.GridStyles.None;
        public DrawingCanvas.GridStyles Style { get { return gridStyle; } set { gridStyle = value; OnRequestRedraw(); } }
        #region DrawingLayer Members

        public void Render(Graphics grPhoto, RectangleF src, RectangleF to, float zoom)
        {
            if (gridStyle == DrawingCanvas.GridStyles.None) return;

            Pen p1 = null;
            Pen p2 = null;
            Pen p3 = null;
            Pen p4 = null;
            bool dots = false;
            float pw = zoom >= gridLimit ? 3 : 1;
            switch (gridStyle)
            {
                case DrawingCanvas.GridStyles.None:
                    return;
                case DrawingCanvas.GridStyles.Dash:
                    p1 = new Pen(Color.White, 1);
                    p2 = new Pen(Color.White, pw);
                    p3 = new Pen(Color.Black, 1);
                    p3.DashStyle = DashStyle.Dot;
                    p4 = new Pen(Color.Black, pw);
                    p4.DashStyle = DashStyle.Dash;
                    dots = true;
                    break;
                case DrawingCanvas.GridStyles.Black:
                    p1 = new Pen(Color.Black, 1);
                    p1.DashStyle = DashStyle.Solid;
                    p2 = new Pen(Color.Black, pw);
                    break;
                case DrawingCanvas.GridStyles.White:
                    p1 = new Pen(Color.White, 1);
                    p2 = new Pen(Color.White, pw);
                    break;
                default:
                    break;
            }
            float width = to.Width;
            float height = to.Height;
            // draw fine grid first if zoom is high enough
            if (zoom >= gridLimit)
            {
                for (float i = 0; i <= height; i += zoom)
                {
                    grPhoto.DrawLine(p1, 0, i, width, i);
                    if (dots)
                        grPhoto.DrawLine(p3, 0, i, width, i);
                }

                for (float i = 0; i <= width; i += zoom)
                {
                    grPhoto.DrawLine(p1, i, 0, i, height);
                    if (dots)
                        grPhoto.DrawLine(p3, i, 0, i, height);
                }
            }
            float first = zoom * ((10 - src.Y % 10) % 10);
            // Draw sparse grid over every tenth pixel
            for (float i = first; i <= height; i += zoom * 10)
            {
                grPhoto.DrawLine(p2, 0, i, width, i);
                if (dots)
                    grPhoto.DrawLine(p4, 0, i, width, i);
            }

            first = zoom * ((10 - src.X % 10) % 10);
            for (float i = first; i <= width; i += zoom * 10)
            {
                grPhoto.DrawLine(p2, i, 0, i, height);
                if (dots)
                    grPhoto.DrawLine(p4, i, 0, i, height);
            }
            if (p1 != null) p1.Dispose();
            if (p2 != null) p2.Dispose();
            if (p3 != null) p3.Dispose();
            if (p4 != null) p4.Dispose();

        }

        public event EventHandler RequestRedraw;
        void OnRequestRedraw() { if (RequestRedraw != null) RequestRedraw(this, EventArgs.Empty); }

        #endregion
    }

    public class CropCueLayer : DrawingCanvas.DrawingLayer
    {
        /// <summary>
        /// Crop rectangle in picture's pixel coordinates
        /// </summary>
        private Rectangle cropRect = new Rectangle(0, 0, 0, 0);
        public Rectangle CropRect { get { return cropRect; } set { cropRect = value; OnRequestRedraw(); } }
        Pen pen = Pens.Gray;
        public Pen CropPen { get { return pen; } set { pen = value; } }
        #region DrawingLayer Members

        public void Render(Graphics g, RectangleF src, RectangleF to, float zoom)
        {
            if (cropRect.Width == 0) return;
            // draw rectangle to right place 
            RectangleF r = new RectangleF( 
                zoom *(cropRect.Left-src.Left),
                zoom *(cropRect.Top-src.Top), 
                zoom * cropRect.Width,
                zoom * cropRect.Height);
            
            g.DrawRectangle(pen, r.X, r.Y, r.Width, r.Height);
        }

        public event EventHandler RequestRedraw;
        void OnRequestRedraw() { if (RequestRedraw != null) RequestRedraw(this, EventArgs.Empty); }

        #endregion
    };
}

/* COLORMATRIX EXAMPLE
                    ColorMatrix cm = new ColorMatrix(
                          new float[][]
                          {
                             new float[] {.3f, .3f, .3f, 0, 0},
                             new float[] {.59f, .59f, .59f, 0, 0},
                             new float[] {.11f, .11f, .11f, 0, 0},
                             new float[] {0, 0, 0, 1, 0},
                             new float[] {0, 0, 0, 0, 1}
                          });
                    attr.SetColorMatrix(cm);
                     * */
